/**
 * Copyright (c) 2020 kedacom
 * OpenATC is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 * http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 **/
package com.openatc.agent.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.openatc.agent.model.TokenModel;
import com.openatc.agent.model.User;
import com.openatc.agent.service.AscsDao;
import com.openatc.agent.service.UserDao;
import com.openatc.model.model.AscsBaseModel;
import org.apache.shiro.authc.AuthenticationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static com.openatc.core.common.Constants.REQUEST_HEADER_TOKEN_KEY;
import static com.openatc.core.common.IErrorEnumImplOuter.E_3014;
import static com.openatc.core.common.IErrorEnumImplOuter.E_3025;

@Component
public class TokenUtil {

    // String为token的内容
//    private static volatile Map<String, TokenModel> tokenMap = new HashMap<>();
    // 缓存中只存放手动禁用的token
    private static Map<String, TokenModel> tokenMap = new ConcurrentHashMap<>();

    private Logger logger = LoggerFactory.getLogger(TokenUtil.class);

    @Value("${jwt.token.secret:kedacom}")
    private String secret;

    @Value("${jwt.token.expiration:43200}")
    private Long expiration; // 12 hour default

    @Autowired
    private UserDao userDao;

    @Autowired
    private AscsDao ascsDao;

    private Algorithm algorithm = null;

    private JWTVerifier verifier = null;

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public Long getExpiration() {
        return expiration;
    }

    public void setExpiration(Long expiration) {
        this.expiration = expiration;
    }

    private synchronized Algorithm getAlgorithm() {
        if (algorithm == null) {
            algorithm = Algorithm.HMAC256(secret);
        }
        return algorithm;
    }

    private synchronized JWTVerifier getVerifier() {
        if (verifier == null) {
            verifier = JWT.require(getAlgorithm()).build();
        }
        return verifier;
    }

    /**
     * 生成token
     *
     * @param username 用户名
     * @return
     */
    public String generateToken(String username, Long timeStamp) {
        return generateToken(username,timeStamp,generateExpirationDate(),new Date());
    }


    /**
     * 生成token
     * @param userName 用户名
     * @param timeStamp 时间戳
     * @param expiresAt 过期时间
     * @param issuedAt 创建时间
     * @return
     */
    public String generateToken(String userName,Long timeStamp,Date expiresAt,Date issuedAt) {
        return JWT.create()
                .withSubject(userName)
                .withIssuedAt(issuedAt)
                .withExpiresAt(expiresAt)
                .withKeyId(timeStamp.toString())
                .sign(getAlgorithm());
    }


    /**
     * 生成token时间 = 当前时间 + expiration（properties中配置的失效时间）
     *
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }


    /**
     * 验证token是否处于启用状态
     * @param token
     * @return
     */
    public static boolean isTokenUsable(String token) {
        // 如果tokenMap中没有包含当前token，认为启用
        if (!tokenMap.containsKey(token)) {
            return true;
        }
        // 如果tokenMap中存在当前token，验证其是否启用，避免存在没有更新缓存的地方导致程序出错
        TokenModel tokenModel = tokenMap.get(token);
        return tokenModel.getIsValid();
    }
    /**
     * 根据token获取用户名
     *
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {

        if(token == null){
            return null;
        }

        try {
            DecodedJWT jwt = getVerifier().verify(token);
            return jwt.getSubject();
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            logger.error("getUsernameFromToken error:{}", exception.getMessage());
            return null;
        } catch (NullPointerException e) {
            //Invalid signature/claims
            logger.error("getUsernameFromToken error:{}", e.getMessage());
            return null;
        }
    }


    /**
     * Token失效校验
     *
     * @param token token字符串
     * @return
     */
    public Boolean validateToken(String token) {
        if (token == null) {
            return false;
        }
        //判断token是否过期 true表示过期了
//        if (isTokenExpiredEx(token)) {
//            return false;
//        }
        if (!isTokenInValidDate(token)) {
            return false;
        }

        String username = getUsernameFromToken(token);

        if (username == null) {
            return false;
        }

        User user = userDao.getUserByUserName(username);
        //判断用户是否停用
        if (user.getStatus() == 0) {
            return false;
        }
        return true;
    }


    /**
     * 判断token是否过期,并进行续期
     * token 在ExpiresAt之后，判断为过期
     * @param token
     * @return
     */
    public boolean isTokenExpiredEx(String token) throws AuthenticationException {
        try {
            DecodedJWT jwt = getVerifier().verify(token);
            // token续期 todo
            renewal(jwt);
        } catch (JWTVerificationException exception) {
            // tokenExpired
            if (exception instanceof TokenExpiredException) {
                logger.error(exception.getMessage() + "! " + token);
                return true;
            }
        }
        return false;
    }

    /**
     * 判断token是否在有效期
     * token在 ExpiresAt之后以及IssuedAt之前，都属于无效
     * @param token
     * @return
     */
    public boolean isTokenInValidDate(String token) {
        try {
            getVerifier().verify(token);
        } catch (JWTVerificationException exception) {
            // 如果签名认证失败，不认为是有效期导致的失败
            if (exception instanceof SignatureVerificationException)
                return true;
            // tokenExpired
            logger.error(exception.getMessage() + "! token: " + token + " ip: " + HttpUtil.getIpAddress());
            return false;
        }
        return true;
    }

    /**
     * token续期 todo
     * @param djt
     */
    private void renewal(DecodedJWT djt) {
        // 解析出token，如果当前token是登录时颁发的，续期
        // 续期思路：重新颁发token，放如响应头中，应答
    }

    public Set<String> getDevsFromToken(String token) {
        Set<String> AgentidSet = new HashSet<>();

        String username = getUsernameFromToken(token);

        User user = userDao.getUserByUserName(username);
        //判断用户是否停用
        if (user.getStatus() == 0) {
            return AgentidSet;
        }

        // 判断是否是管理员，管理员无设备限制
        List<String> roleNames = userDao.getRoleNamesByUsername(user.getUser_name());
        for(String roleName : roleNames){
            if(roleName.equals("admin") || roleName.equals("superadmin")){
                // 路口为NULL代表管理员
                return null;
            }
        }

        // 其他角色按组织机构分配可订阅的路口权限
        List<AscsBaseModel> ascs = ascsDao.getAscsLikeCode(user.getOrganization());
        for(AscsBaseModel asc : ascs){
            AgentidSet.add(asc.getAgentid());
        }

        return AgentidSet;
    }


    /**
     * 获取当前线程的token
     * @return
     */
    public static String getCurrentUserName() {

        String token = getCurrentToken();

        TokenUtil tokenUtil = ApplicationContextProvider.getBean(TokenUtil.class);
        return tokenUtil.getUsernameFromToken(token);
    }

    /**
     * 获取当前线程的token
     * @return
     */
    public static String getCurrentToken() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = ((ServletRequestAttributes) requestAttributes);
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader(REQUEST_HEADER_TOKEN_KEY);
        return token;
    }

    /**
     * 只能通过当前方法更新token的缓存
     * @param tokenModel token
     */
    public static void updateTokenCache(TokenModel tokenModel) {
        Boolean valid = tokenModel.getIsValid();
        // 1、如果当前token未启用，直接放进去
        if (!valid) {
            tokenMap.put(tokenModel.getToken(),tokenModel);
        } else {
            // 2、如果当前token启用，要从map中移除
            clearToken(tokenModel.getToken());
        }
    }


    /**
     * 清除缓存中指定的token
     * @param token
     */
    public static void clearToken(String token) {
        tokenMap.remove(token);
    }

    /**
     * 返回token的缓存
     * @return
     */
    public static Map<String,TokenModel> getTokenMap() {
        return tokenMap;
    }


    /**
     * 验证token是否在有效期以及是否启用
     * @param token
     * @Excption AuthenticationException
     */
    public static void checkToken(String token) {
        // 验证token是否启用
        if (!isTokenUsable(token)) {
            throw new AuthenticationException(E_3025.getErrorMsg());
        }
        // 验证token是否在有效期
        boolean inValidDate = getTokenUtil().isTokenInValidDate(token);
        if (!inValidDate) {
            throw new AuthenticationException(E_3014.getErrorMsg());
        }
    }

    /**
     * 将tokenUtil中的实例方法重构为静态方法 todo
     * 该方法用于过渡
     * @return
     */
    private static TokenUtil getTokenUtil() {
        return ApplicationContextProvider.getBean(TokenUtil.class);
    }
}
